package fr.tvbarthel.apps.sayitfromthesky.fragments;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.ProgressBar;
import android.widget.Toast;
import android.widget.ToggleButton;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.maps.android.PolyUtil;
import com.google.maps.android.SphericalUtil;
import java.util.ArrayList;
import java.util.List;
import butterknife.ButterKnife;
import butterknife.InjectView;
import fr.tvbarthel.apps.sayitfromthesky.R;
import fr.tvbarthel.apps.sayitfromthesky.helpers.ActionBarHelper;
import fr.tvbarthel.apps.sayitfromthesky.helpers.ViewHelper;
import fr.tvbarthel.apps.sayitfromthesky.models.Drawing;
/**
* A simple {@link android.support.v4.app.Fragment} for drawing a path while walking.
* <p/>
* Test purpose : example of an encoded polyline : qixvGyhqFKRs@P
*/
public class DrawingFragment extends Fragment implements SayItMapFragment.Callback {
private static float DEFAULT_VALUE_ZOOM = 15f;
private static float DELTA_DISTANCE_IN_METER = 5f;
private static float ACURRACY_MINIMUM_IN_METER = 400f;
private static final int REQUEST_CODE_SAVE_PATH = 1;
// Bundle key used for saving instance state
private static String BUNDLE_KEY_LOCATION = "DrawingFragment.Bundle.Key.Location";
private static String BUNDLE_KEY_ZOOM = "DrawingFragment.Bundle.Key.Zoom";
private static String BUNDLE_KEY_CURRENT_POLYLINE = "DrawingFragment.Bundle.Key.Current.Polyline";
private static String BUNDLE_KEY_ENCODED_POLYLINES = "DrawingFragment.Bundle.Key.Other.Polylines";
// UI elements
private SayItMapFragment mMapFragment;
private Toast mTextToast;
@InjectView(R.id.fragment_drawing_button_line_state)
ToggleButton mLineStateButton;
@InjectView(R.id.fragment_drawing_button_add_point)
Button mAddPointButton;
@InjectView(R.id.fragment_drawing_progress_bar)
ProgressBar mProgressBar;
private GoogleMap mGoogleMap;
private Location mLastKnownLocation;
private LatLng mLastKnownLatLng;
private float mLastKnownZoom;
private PolylineOptions mPolylineOptionsCurrent;
private Polyline mCurrentPolyline;
private PolylineOptions mPolylineOptionsPreview;
private Polyline mPreviewPolyline;
private ArrayList<String> mEncodedPolylines;
private Bundle mLastSavedInstanceState;
private boolean mIsCurrentPointInCurrentPath;
private Callback mCallback;
/**
* Default Constructor.
* <p/>
* lint [ValidFragment]
* http://developer.android.com/reference/android/app/Fragment.html#Fragment()
* Every fragment must have an empty constructor, so it can be instantiated when restoring its activity's state.
*/
public DrawingFragment() {
super();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (activity instanceof Callback) {
mCallback = (Callback) activity;
} else {
throw new ClassCastException(activity.toString() + "must implement DrawingFragment.Callback");
}
}
@Override
public void onDetach() {
super.onDetach();
mCallback = null;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Resources resources = getResources();
mLastKnownZoom = DEFAULT_VALUE_ZOOM;
setHasOptionsMenu(true);
// Create the polyline array used to store the polylines added to the map.
mEncodedPolylines = new ArrayList<String>();
// Create the polyline used for the current path.
mPolylineOptionsCurrent = new PolylineOptions();
mPolylineOptionsCurrent.color(resources.getColor(R.color.primary_color));
// Create the polyline for the preview path.
mPolylineOptionsPreview = new PolylineOptions();
mPolylineOptionsPreview.color(resources.getColor(R.color.accent_color));
// The current point/position is not in the current path yet.
mIsCurrentPointInCurrentPath = false;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_drawing, container, false);
ButterKnife.inject(this, view);
// Setup the button used to add a point to the current path.
mAddPointButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addPointToCurrentPolyline(mLastKnownLatLng);
}
});
// Setup the toggle button used to start and stop a path.
mLineStateButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (mGoogleMap != null) {
if (isChecked) {
if (mCurrentPolyline == null) {
mCurrentPolyline = mGoogleMap.addPolyline(mPolylineOptionsCurrent);
}
mPreviewPolyline.setVisible(true);
addPointToCurrentPolyline(mLastKnownLatLng);
mAddPointButton.setVisibility(View.VISIBLE);
ViewHelper.slideFromBottom(mAddPointButton);
} else {
if (mCurrentPolyline != null && mCurrentPolyline.getPoints().size() <= 1) {
// The current polyline has no interest since it only contains one point.
mCurrentPolyline.remove();
} else if (mCurrentPolyline != null) {
// The current polyline is a part of the drawing
mEncodedPolylines.add(PolyUtil.encode(mCurrentPolyline.getPoints()));
}
mCurrentPolyline = null;
mAddPointButton.setVisibility(View.INVISIBLE);
mPreviewPolyline.setVisible(false);
mIsCurrentPointInCurrentPath = false;
}
}
}
});
if (savedInstanceState != null) {
// Store savedInstanceState for future use, when the map will actually be ready.
mLastSavedInstanceState = savedInstanceState;
setLastKnownLocation((Location) savedInstanceState.getParcelable(BUNDLE_KEY_LOCATION));
mLastKnownZoom = savedInstanceState.getFloat(BUNDLE_KEY_ZOOM, DEFAULT_VALUE_ZOOM);
}
mMapFragment = (SayItMapFragment) getChildFragmentManager().findFragmentByTag("fragmentTagMap");
if (mMapFragment == null) {
// Create a new map fragment.
mMapFragment = new SayItMapFragment();
getChildFragmentManager().beginTransaction().add(R.id.fragment_drawing_map_container, mMapFragment, "fragmentTagMap").commit();
} else {
getChildFragmentManager().beginTransaction().show(mMapFragment).commit();
}
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
ButterKnife.reset(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_KEY_LOCATION, mLastKnownLocation);
if (mGoogleMap != null) {
outState.putFloat(BUNDLE_KEY_ZOOM, mGoogleMap.getCameraPosition().zoom);
}
saveCurrentPolyline(outState);
saveEncodedPolyline(outState);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.drawing, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_save) {
if ((mEncodedPolylines != null && mEncodedPolylines.size() > 0)
|| (mCurrentPolyline != null && mCurrentPolyline.getPoints().size() > 1)) {
saveCurrentDrawing();
} else {
// TODO don't use hard coded String
makeToast("There is nothing to save !");
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SAVE_PATH && resultCode == Activity.RESULT_OK) {
// The save path request has been done.
// TODO reset the current path and the other paths after
}
}
@Override
public void onMapReady() {
if (mGoogleMap == null) {
mGoogleMap = mMapFragment.getMap();
if (mGoogleMap != null) {
// The map is now active and can be manipulated
// Enable my location
mGoogleMap.setMyLocationEnabled(true);
// Setup the map UI
final UiSettings uiSettings = mGoogleMap.getUiSettings();
uiSettings.setCompassEnabled(false);
uiSettings.setZoomControlsEnabled(false);
uiSettings.setMyLocationButtonEnabled(true);
int actionBarSize = ActionBarHelper.getActionBarSize(getActivity());
mGoogleMap.setPadding(0, actionBarSize, 0, 0);
// Add the preview polyline
mPreviewPolyline = mGoogleMap.addPolyline(mPolylineOptionsPreview);
// Restore the polylines that were displayed on the map
// Add ask to draw them
if (mLastSavedInstanceState != null) {
restoreEncodedPolyline(true);
restoreCurrentPolyline(true);
}
// Try to restore last known location
if (mLastKnownLocation != null) {
initMapLocation();
}
mGoogleMap.setOnMyLocationChangeListener(new GoogleMap.OnMyLocationChangeListener() {
@Override
public void onMyLocationChange(Location location) {
if (mProgressBar.getVisibility() != View.GONE) {
mProgressBar.setVisibility(View.GONE);
}
if (mLastKnownLocation == null) {
// First location
setLastKnownLocation(location);
initMapLocation();
} else {
// New location
setNewLocation(location);
}
}
});
}
} else {
// Setup the circle buttons
initCircleButtons();
}
}
@Override
public void onPause() {
super.onPause();
hideToast();
}
private void makeToast(String message) {
hideToast();
mTextToast = Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT);
mTextToast.show();
}
private void hideToast() {
if (mTextToast != null) {
mTextToast.cancel();
mTextToast = null;
}
}
private void saveCurrentDrawing() {
final ArrayList<String> encodedPaths = new ArrayList<String>();
encodedPaths.addAll(mEncodedPolylines);
if (mCurrentPolyline != null) {
// Add the current polyline
encodedPaths.add(PolyUtil.encode(mCurrentPolyline.getPoints()));
}
final Drawing drawingToSave = new Drawing("Default title", System.currentTimeMillis(), encodedPaths);
mCallback.saveDrawing(drawingToSave);
}
private void saveCurrentPolyline(Bundle outState) {
if (mCurrentPolyline != null) {
outState.putString(BUNDLE_KEY_CURRENT_POLYLINE, PolyUtil.encode(mCurrentPolyline.getPoints()));
}
}
/**
* Restore the current polyline from the last savedInstanceState.
* This method should be called only after the map is ready.
*/
private void restoreCurrentPolyline(boolean shouldDrawPolyline) {
final String encodedPoints = mLastSavedInstanceState.getString(BUNDLE_KEY_CURRENT_POLYLINE);
if (encodedPoints != null && shouldDrawPolyline) {
mCurrentPolyline = mGoogleMap.addPolyline(mPolylineOptionsCurrent);
mCurrentPolyline.setPoints(PolyUtil.decode(encodedPoints));
}
}
private void saveEncodedPolyline(Bundle outState) {
if (mEncodedPolylines != null && mEncodedPolylines.size() > 0) {
outState.putStringArrayList(BUNDLE_KEY_ENCODED_POLYLINES, mEncodedPolylines);
}
}
private void restoreEncodedPolyline(boolean shouldDrawPolyline) {
mEncodedPolylines = mLastSavedInstanceState.getStringArrayList(BUNDLE_KEY_ENCODED_POLYLINES);
if (mEncodedPolylines == null) {
mEncodedPolylines = new ArrayList<String>();
} else if (shouldDrawPolyline) {
for (String encodedPolyline : mEncodedPolylines) {
mGoogleMap.addPolyline(mPolylineOptionsCurrent).setPoints(PolyUtil.decode(encodedPolyline));
}
}
}
private void initCircleButtons() {
mLineStateButton.setVisibility(View.VISIBLE);
ViewHelper.slideFromBottom(mLineStateButton);
}
private void initMapLocation() {
initCircleButtons();
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(mLastKnownLatLng, mLastKnownZoom));
}
private boolean isLocationOutdated(Location candidateLocation) {
boolean isLocationOutdated = false;
if (candidateLocation.getAccuracy() < mLastKnownLocation.getAccuracy()) {
isLocationOutdated = true;
} else if (candidateLocation.getAccuracy() < ACURRACY_MINIMUM_IN_METER) {
final LatLng candidateLatLng = locationToLatLng(candidateLocation);
final double distance = SphericalUtil.computeDistanceBetween(candidateLatLng, mLastKnownLatLng);
isLocationOutdated = distance > DELTA_DISTANCE_IN_METER;
}
return isLocationOutdated;
}
private void setNewLocation(Location newLocation) {
setLastKnownLocation(newLocation);
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLng(mLastKnownLatLng));
}
private void setLastKnownLocation(Location location) {
if (location != null) {
mLastKnownLocation = location;
mLastKnownLatLng = locationToLatLng(location);
mIsCurrentPointInCurrentPath = false;
updatePreviewPoints();
}
}
private LatLng locationToLatLng(Location location) {
return new LatLng(location.getLatitude(), location.getLongitude());
}
private void addPointToCurrentPolyline(LatLng newPoint) {
if (!mIsCurrentPointInCurrentPath) {
final List<LatLng> currentPoints = mCurrentPolyline.getPoints();
currentPoints.add(newPoint);
mCurrentPolyline.setPoints(currentPoints);
updatePreviewPoints();
mIsCurrentPointInCurrentPath = true;
}
}
private void updatePreviewPoints() {
if (mCurrentPolyline != null) {
final List<LatLng> previewPoints = new ArrayList<LatLng>();
final List<LatLng> currentPoints = mCurrentPolyline.getPoints();
previewPoints.add(currentPoints.get(currentPoints.size() - 1));
previewPoints.add(mLastKnownLatLng);
mPreviewPolyline.setPoints(previewPoints);
}
}
/**
* Interface definition for a callback.
*/
public static interface Callback {
/**
* Called when a {@link fr.tvbarthel.apps.sayitfromthesky.models.Drawing} has to be saved.
*
* @param drawingToSave the drawing to save.
*/
void saveDrawing(Drawing drawingToSave);
}
}